----------The Spy Strikes Back---------
A 4am crack                  2018-08-04
---------------------------------------

Name: The Spy Strikes Back!
Genre: arcade
Year: 1983
Credits: Robert Hardy, Mark Pelczarski
Publisher: Penguin Software
Platform: Apple ][+ or later
Media: 5.25-inch disk
Sides: 1
OS: custom
Similar cracks:
  #1748 Bouncing Kamungas
  #1676 Thunder Bombs

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  immediate disk read error, but it
  gets a participation certificate that
  spells out "You Tried"

Locksmith Fast Disk Backup
  unable to read any track

EDD 4 bit copy to .nib file on CFFA3000
  read errors on T12+
  copy fails to boot, hangs with the
  drive motor off

Copy ][+ nibble editor
  tracks $12-$22 appear unformatted
  Lower tracks have modified epilogues
    ("DA AA EB" instead of "DE AA EB")
  Odd tracks also have custom prologue
    ("D4 AA 96" instead of "D5 AA 96")
  no sign of anything beyond track $11
  no half or quarter tracks

Disk Fixer
  "O" for INPUT/OUTPUT CONTROL, set the
    epilogues to "DA AA EB"
  track 0 is readable
  boot sector is custom
  no sign of disk catalog on any track
  no sign of DOS or ProDOS or any OS
    whatsoever

Why didn't COPYA work?
  modified prologues/epilogues on
    every track

Why didn't Locksmith FDB work?
  ditto

Why didn't my .nib image work?
  This is a bit of a mystery. The
  alternating D4/D5 prologues shouldn't
  pose any problem for a nibble copier,
  so I'm assuming there is some sort of
  nibble check early in the boot.

Next steps:

  1. Run Passport to convert the disk
     to a standard format
  2. Patch the bootloader to read the
     newly standardized disk
  3. Find and disable the nibble check
  4. Declare victory (*)

                   ~

               Chapter 1
 In Which Our Automated Tools Take Us
     Just About As Far As They Can


Passport successfully auto-converted
the disk to a standard format. The
built-in RWTS was designed to read this
kind of alternating D4/D5 prologue (it
was very common), and the adaptive RWTS
correctly determined the first epilogue
nibble (DA) and enforced it on the rest
of the sectors.

More information and source code is
available at
https://archive.org/details/Passport4am

Here is the Passport transcript:

                 --v--

READING FROM S6,D1
USING BUILT-IN RWTS
T22 IS UNFORMATTED
WRITING TO RAM DISK
T21 IS UNFORMATTED
T20 IS UNFORMATTED
T1F IS UNFORMATTED
T1E IS UNFORMATTED
T1D IS UNFORMATTED
T1C IS UNFORMATTED
T1B IS UNFORMATTED
T1A IS UNFORMATTED
T19 IS UNFORMATTED
T18 IS UNFORMATTED
T17 IS UNFORMATTED
T16 IS UNFORMATTED
T15 IS UNFORMATTED
T14 IS UNFORMATTED
T13 IS UNFORMATTED
T12 IS UNFORMATTED
WRITING TO S5,D2

THE DISK WAS COPIED SUCCESSFULLY, BUT
PASSPORT DID NOT APPLY ANY PATCHES.

POSSIBLE REASONS:
- THE SOURCE DISK IS NOT COPY PROTECTED.
- THE TARGET DISK WORKS WITHOUT PATCHES.
- THE DISK USES AN UNKNOWN PROTECTION,
  AND PASSPORT CAN NOT HELP ANY FURTHER.

                 --^--

The copy that Passport produces can not
read itself, which is not entirely
unexpected. It is most likely enforcing
the custom "DA AA EB" epilogue, which
is now the standard "DE AA EB". A quick
sector search for "BD 8C C0" (the
standard opcode to read the data latch
from a drive indexed by the X register)
found a DOS-shaped RWTS on track $04,
which is interesting but not the code I
was looking for. (My failed copy never
gets off track 0.) So, putting a pin in
the fact that there is a secondary RWTS
that may be used later once the game is
loaded, I set off to trace the boot.

                   ~

               Chapter 2
         Boot Trace and Chill


[S6,D1=original disk]
[S5,D1=my work disk]

]PR#5
...
]CALL -151

*9600<C600.C6FFM

; copy boot sector to higher memory so
; it survives a reboot to my work disk
96F8-   A0 00       LDY   #$00
96FA-   B9 00 08    LDA   $0800,Y
96FD-   99 00 28    STA   $2800,Y
9700-   C8          INY
9701-   D0 F7       BNE   $96FA

; turn off slot 6 drive motor
9703-   AD E8 C0    LDA   $C0E8

; reboot to my work disk
9706-   4C 00 C5    JMP   $C500

*BSAVE TRACE,A$9600,L$109
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0800-08FF,A$2800,L$100
]CALL -151

; move the boot sector back into place
*800<2800.28FFM
*801L

0801-   D8          CLD

; read ROM, write LC RAM bank 2
0802-   2C 81 C0    BIT   $C081

; check $FF58, a known location of an
; "RTS" opcode ($60) on all Apple II
; models
0805-   A9 60       LDA   #$60
0807-   4D 58 FF    EOR   $FF58

; if not found, hang forever (this is
; a defense against modified ROM chips
; that were popular in the Apple II
; underground)
080A-   D0 FE       BNE   $080A

; munge reset vector
080C-   8D F3 03    STA   $03F3

; disable interrupts
080F-   78          SEI

; re-use drive firmware at $Cx5C to
; read a few more sectors from track 0
0810-   AD 00 08    LDA   $0800
0813-   C9 06       CMP   #$06

; exit via $88A when all sectors have
; been read
0815-   B0 73       BCS   $088A

; calculate re-entry point for calling
; the drive firmware, based on the boot
; slot
0817-   69 02       ADC   #$02
0819-   8D 00 08    STA   $0800
081C-   E6 3D       INC   $3D
081E-   8A          TXA
081F-   4A          LSR
0820-   4A          LSR
0821-   4A          LSR
0822-   4A          LSR
0823-   09 C0       ORA   #$C0
0825-   8D 2A 08    STA   $082A

; call the drive firmware to read the
; next sector (exits via $0801, so this
; whole thing is a loop that only exits
; via the BCS at $0815)
0828-   4C 5C C6    JMP   $C65C

Execution continues at $088A once we've
read [...counts on fingers] three more
sectors into $0900..$0BFF.

*88AL

; turn off any peripheral ROM at $C800
088A-   2C FF CF    BIT   $CFFF

; wipe zero page
088D-   98          TYA
088E-   99 00 00    STA   $0000,Y
0891-   C8          INY
0892-   D0 FA       BNE   $088E

; save boot slot (x16) to a custom zero
; page location
0894-   86 30       STX   $30

; clear both hi-res screens
0896-   A2 3F       LDX   #$3F
0898-   99 00 20    STA   $2000,Y
089B-   C8          INY
089C-   D0 FA       BNE   $0898
089E-   EE 9A 08    INC   $089A
08A1-   CA          DEX
08A2-   10 F4       BPL   $0898

; reset stack
08A4-   9A          TXS

; IN#0, PR#0, TEXT, &c.
08A5-   8E FB 04    STX   $04FB
08A8-   8E 0C C0    STX   $C00C
08AB-   8E 0E C0    STX   $C00E
08AE-   8E 00 C0    STX   $C000
08B1-   20 89 FE    JSR   $FE89
08B4-   20 93 FE    JSR   $FE93
08B7-   20 84 FE    JSR   $FE84
08BA-   20 2F FB    JSR   $FB2F

; show hi-res screen 2
08BD-   2C 57 C0    BIT   $C057
08C0-   2C 52 C0    BIT   $C052
08C3-   2C 50 C0    BIT   $C050
08C6-   2C 55 C0    BIT   $C055

; read ROM, write LC RAM bank 2
08C9-   2C 81 C0    BIT   $C081
08CC-   2C 81 C0    BIT   $C081

; wipe page 3 vectors
08CF-   A9 D8       LDA   #$D8
08D1-   A8          TAY
08D2-   99 00 03    STA   $0300,Y
08D5-   C8          INY
08D6-   D0 FA       BNE   $08D2

; wipe language card (defense
; against software-based boot
tracers like Inspector)
08D8-   B9 00 D0    LDA   $D000,Y
08DB-   99 00 D0    STA   $D000,Y
08DE-   88          DEY
08DF-   D0 F7       BNE   $08D8
08E1-   EE DA 08    INC   $08DA
08E4-   EE DD 08    INC   $08DD
08E7-   D0 EF       BNE   $08D8

; not sure what this is
08E9-   A9 02       LDA   #$02
08EB-   8D 10 01    STA   $0110
08EE-   8D 00 BD    STA   $BD00

; start setting page 3 vectors to point
; to $110
08F1-   A0 10       LDY   #$10
08F3-   A9 01       LDA   #$01
08F5-   8C F0 03    STY   $03F0
08F8-   8D F1 03    STA   $03F1
08FB-   8C FE 03    STY   $03FE

And that's the end of the first boot
sector. Execution continues, but I
don't have $0900+ in memory yet.

Let's fix that.

*9600<C600.C6FFM

; set up a callback at $087A, after the
; boot sector reads additional sectors
; into $0900..$0BFF
96F8-   A9 4C       LDA   #$4C
96FA-   8D 8A 08    STA   $088A
96FD-   A9 0A       LDA   #$0A
96FF-   8D 8B 08    STA   $088B
9702-   A9 97       LDA   #$97
9704-   8D 8C 08    STA   $088C

; start the boot
9707-   4C 01 08    JMP   $0801

; callback is here --
; copy all the boot sectors to higher
; memory
970A-   A2 04       LDX   #$04
970C-   A0 00       LDY   #$00
970E-   B9 00 08    LDA   $0800,Y
9711-   99 00 28    STA   $2800,Y
9714-   C8          INY
9715-   D0 F7       BNE   $970E
9717-   EE 10 97    INC   $9710
971A-   EE 13 97    INC   $9713
971D-   CA          DEX
971E-   D0 EE       BNE   $970E

; turn off drive motor and reboot to
; my work disk
9720-   AD E8 C0    LDA   $C0E8
9723-   4C 00 C5    JMP   $C500

*BSAVE TRACE2,A$9600,L$126
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE OBJ.0800-0BFF,A$2800,L$400
]CALL -151

; restore boot code to original address
*800<2800.2BFFM

Now we can continue with the listing,
right where we left off.

                   ~

               Chapter 3
        asdfjasklhfuiafgjklsfj


*8F1L

; set lots of page 3 vectors (NMI, BRK,
; reset) and some low-level vectors too
; so everything points to $0110
; (defense against NMI capture cards
; like Wildcard that had a button you
; could press to break to the monitor
; and capture the game in memory)
08F1-   A0 10       LDY   #$10
08F3-   A9 01       LDA   #$01
08F5-   8C F0 03    STY   $03F0
08F8-   8D F1 03    STA   $03F1
08FB-   8C FE 03    STY   $03FE
08FE-   8D FF 03    STA   $03FF
0901-   8C F2 03    STY   $03F2
0904-   8D F3 03    STA   $03F3
0907-   8C FA FF    STY   $FFFA
090A-   8D FB FF    STA   $FFFB
090D-   8C FC FF    STY   $FFFC
0910-   8D FD FF    STA   $FFFD

; read LC RAM bank 2, no write
; (this remains active, so any attempt
; to hit Ctrl-Reset will be directed
; through the low-level reset vector
; at $FFFC, which we just set)
0913-   2C 80 C0    BIT   $C080

; finish setting every possible vector
; everywhere
0916-   49 A5       EOR   #$A5
0918-   8D F4 03    STA   $03F4
091B-   A9 4C       LDA   #$4C
091D-   8D EF 03    STA   $03EF
0920-   8D FD 03    STA   $03FD

; we literally just set the low-level
; reset vector (at $090D), and now we
; are checking whether it's changed
0923-   CC FC FF    CPY   $FFFC

; no change, continue
0926-   F0 08       BEQ   $0930

; check if another known location is a
; known value ($E000, which is usually
; $4C, a JMP operation)
0928-   CD 00 E0    CMP   $E000

; yes, that checks out
092B-   F0 03       BEQ   $0930

; neither known value checked out,
; which probably means this machine
; only has 48K (no language card, so no
; upper 16K, so we've been storing
; values into thin air this whole time)
; so switch back to ROM and be happy
092D-   2C 81 C0    BIT   $C081

; execution continues here in any case
; (on machines with 64K, LC RAM bank 2
; is still active and the custom low-
; level reset vectors are in place) and
; OH GOD WHAT'S ALL THIS
0930-   A2 5C       LDX   #$5C
0932-   5D 2D 08    EOR   $082D,X
0935-   9D 10 01    STA   $0110,X
0938-   CA          DEX
0939-   10 F7       BPL   $0932

; OH GOD WHAT'S ALL THIS TOO
093B-   A0 03       LDY   #$03
093D-   AE 2B 08    LDX   $082B
0940-   5D 00 09    EOR   $0900,X
0943-   9D 00 05    STA   $0500,X
0946-   E8          INX
0947-   D0 F7       BNE   $0940
0949-   EE 42 09    INC   $0942
094C-   EE 45 09    INC   $0945
094F-   88          DEY
0950-   D0 EE       BNE   $0940

the rest of the bootloader is encrypted
asdfjasklhfuiafgjklsfj that's great drm
is great grrrrrrrrrrrrrrrrrrrrrrrr okay
here we go

; set some stuff in the code we just
decrypted onto the stack
0952-   A5 30       LDA   $30
0954-   09 89       ORA   #$89
0956-   8D 11 01    STA   $0111
0959-   AD 2A 08    LDA   $082A
095C-   8D 68 01    STA   $0168

; and continue inside the decrypted
; code on the text page
095F-   4C 92 07    JMP   $0792

I can't ignore it any longer. It's time
to decrypt the bootloader.

; restore the original boot sector (the
; one in memory has my trace callback
; in it)
*BLOAD OBJ.0800-08FF,A$800

I can't modify the decryption loop,
because it forms the basis for the key
to decrypt later pages.

So that's great.

But I can move the decryption loop and
modify the copy.

; copy code to $2900
*2900<900.9FFM

; fix up the absolute addresses so we
; decrypt into $2100 instead of $0100
*2937:21

; $2500 instead of $0500
*2945:25

; and self-modify our loop at $2900
; instead of $0900
*294B:29
*294E:29

; the initial key is in the accumulator
; (set at $091B before some other stuff
; I don't want to run) so I'll manually
; set it here at the beginning of the
; decryption -- it's $4C
*292D:EA A9 4C

; fix up the post-decryption patchups
*2952:A9 60
*2958:21
*295E:21

; "RTS" instead of "JMP $0792"
*295F:60

The final patched decryption code looks
like this:

*292DL

292D-   EA          NOP
292E-   A9 4C       LDA   #$4C
2930-   A2 5C       LDX   #$5C
2932-   5D 2D 08    EOR   $082D,X
2935-   9D 10 21    STA   $2110,X
2938-   CA          DEX
2939-   10 F7       BPL   $2932
293B-   A0 03       LDY   #$03
293D-   AE 2B 08    LDX   $082B
2940-   5D 00 09    EOR   $0900,X
2943-   9D 00 25    STA   $2500,X
2946-   E8          INX
2947-   D0 F7       BNE   $2940
2949-   EE 42 29    INC   $2942
294C-   EE 45 29    INC   $2945
294F-   88          DEY
2950-   D0 EE       BNE   $2940
2952-   A9 60       LDA   #$60
2954-   09 89       ORA   #$89
2956-   8D 11 21    STA   $2111
2959-   AD 2A 08    LDA   $082A
295C-   8D 68 21    STA   $2168
295F-   60          RTS

Let's go!

*292DG

*2110L

; looks like a Badlands routine ends
; up at $0110 -- this is what all the
; reset and other vectors were pointing
; to
2110-   AC E9 C0    LDY   $C0E9

; clear text screen
2113-   A9 28       LDA   #$28
2115-   85 21       STA   $21
2117-   A9 18       LDA   #$18
2119-   85 23       STA   $23
211B-   A9 00       LDA   #$00
211D-   85 20       STA   $20
211F-   85 22       STA   $22
2121-   85 24       STA   $24
2123-   85 25       STA   $25
2125-   20 58 FC    JSR   $FC58

; show hi-res page 2
2128-   AE 55 C0    LDX   $C055
212B-   AE 52 C0    LDX   $C052
212E-   AE 57 C0    LDX   $C057
2131-   AE 50 C0    LDX   $C050

; wipe every other byte of memory (by
; which I mean every alternating byte,
; because we increment the Y index
; twice, which is a little weird but
; whatever, if we end up here we've
; already lost)
2134-   A0 00       LDY   #$00
2136-   84 4E       STY   $4E
2138-   A9 02       LDA   #$02
213A-   A2 08       LDX   #$08
213C-   86 4F       STX   $4F
213E-   91 4E       STA   ($4E),Y
2140-   C8          INY
2141-   C8          INY
2142-   D0 FA       BNE   $213E
2144-   E8          INX

; skip hi-res screens for some reason
2145-   E0 20       CPX   #$20
2147-   F0 20       BEQ   $2169
2149-   E0 C0       CPX   #$C0
214B-   90 EF       BCC   $213C
214D-   EC 81 C0    CPX   $C081
2150-   AC 81 C0    LDY   $C081

; wipe wipe wipe your RAM,
; gently down the board
2153-   9D 00 03    STA   $0300,X
2156-   BD 00 FF    LDA   $FF00,X
2159-   9D 00 FF    STA   $FF00,X
215C-   E8          INX
215D-   D0 F4       BNE   $2153
215F-   CA          DEX
2160-   8E FB 04    STX   $04FB
2163-   8E 0E C0    STX   $C00E

; reboot from whence we came (this JMP
; address was actually modified by the
; post-decryption patchups at $093C,
; to maintain slot independence even in
; the face of catastrophic failure)
2166-   4C 00 C6    JMP   $C600
2169-   A2 60       LDX   #$60
216B-   D0 CF       BNE   $213C

OK, let's not end up there.

*2792L

2792-   A0 00       LDY   #$00
2794-   A9 02       LDA   #$02
2796-   99 00 08    STA   $0800,Y
2799-   99 00 09    STA   $0900,Y
279C-   99 00 0A    STA   $0A00,Y
279F-   99 00 0B    STA   $0B00,Y
27A2-   C8          INY
27A3-   D0 F1       BNE   $2796
27A5-   85 0A       STA   $0A
27A7-   85 02       STA   $02
27A9-   A9 BE       LDA   #$BE
27AB-   85 05       STA   $05
27AD-   20 20 07    JSR   $0720
27B0-   AD 40 06    LDA   $0640
27B3-   48          PHA
27B4-   AD 54 C0    LDA   $C054
27B7-   20 C2 06    JSR   $06C2

I don't know what that does yet, but it
sure does look like real code, so I
think the decryption was a success.

                   ~

               Chapter 4
     Oh Look You Made A Save Icon


Let's save what we have so far, since
someone went to all this trouble to
prevent us from seeing it.

*BSAVE OBJ.0100-01FF,A$2100,L$100
*BSAVE OBJ.0500-07FF,A$2500,L$300

What I really want is a disk with this
decrypted code on it, paired with a
bootloader that no longer tries to
decrypt it. Then I can start making the
rest of the changes to make it boot.

I have everything in memory -- the
unencrypted parts and the decrypted
parts -- but not all in one place. So
the next step is to combine them.

; copy decrypted code back to page 8
*82D<2110.216CM

; copy decrypted code back to page 9+
*962<2562.27FFM

; change "EOR" instructions to "LDA" --
; abracadabra! the decryption loops
; become copy loops
*932:BD
*940:BD

*BSAVE OBJ.0800-0BFF DECRYPTED,
 A$800,L$400

Now let's write that decrypted version
back to my copy.

; loop through all sectors on track 0,
; writing the ones we want and skipping
; the rest
28C0-   AC ED 28    LDY   $28ED
28C3-   B9 D8 28    LDA   $28D8,Y
28C6-   30 0A       BMI   $28D2
28C8-   8D F1 28    STA   $28F1
28CB-   A9 28       LDA   #$28
28CD-   A0 E8       LDY   #$E8
28CF-   20 D9 03    JSR   $03D9
28D2-   CE ED 28    DEC   $28ED
28D5-   10 E9       BPL   $28C0
28D7-   60          RTS

*28D8.28E7

; T00,S00 <- $0800
; T00,S0E <- $0900
; T00,S0D <- $0A00
; T00,S0C <- $0B00
28D8- 08 FF FF FF FF FF FF FF
28E0- FF FF FF FF 0B 0A 09 FF

*28E8.28FF

; standard RWTS parameter table
28E8- 01 60 01 00 00 0F FB 28
28F0- 00 00 00 00 02 00 FE 60
28F8- 01 00 00 00 01 EF D8 00

*BSAVE WRITE,A$28C0,L$40

[S6,D1=non-working copy]

*28C0G
...write write write...

Now we have a copy that is just as non-
functional as my previous copy, but at
least it's no longer encrypted.

                   ~

               Chapter 5
           Are We There Yet?


Now I get to normalize the newly
decrypted bootloader so my copy can
read itself. The code to match and
parse the address field starts at $0562
(in memory at $2562):

*2562L

; boot slot (x16)
2562-   A6 30       LDX   $30

; get first prologue nibble
2564-   BD 8C C0    LDA   $C08C,X
2567-   10 FB       BPL   $2564

; side effect -- stores $D4 or $D5 in
; zero page, might be relevant later
; [THIS LITERARY TECHNIQUE IS CALLED
;  4SHADOWING]
2569-   85 01       STA   $01
256B-   EA          NOP
256C-   EA          NOP

; match $D4 or $D5 (should be fine,
; since it will still match $D5 on my
; copy), then $AA $96 as normal
256D-   4A          LSR
256E-   49 6A       EOR   #$6A
2570-   D0 F2       BNE   $2564
2572-   BD 8C C0    LDA   $C08C,X
2575-   10 FB       BPL   $2572
2577-   C9 AA       CMP   #$AA
2579-   D0 E9       BNE   $2564
257B-   EA          NOP
257C-   BD 8C C0    LDA   $C08C,X
257F-   10 FB       BPL   $257C
2581-   49 96       EOR   #$96
2583-   D0 DF       BNE   $2564

; parse address field and store the
; disk volume, track, sector, and
; checksum in zero page $2C..$2F
2585-   A0 03       LDY   #$03
2587-   85 0C       STA   $0C
2589-   BD 8C C0    LDA   $C08C,X
258C-   10 FB       BPL   $2589
258E-   2A          ROL
258F-   85 0B       STA   $0B
2591-   BD 8C C0    LDA   $C08C,X
2594-   10 FB       BPL   $2591
2596-   25 0B       AND   $0B
2598-   99 2C 00    STA   $002C,Y
259B-   45 0C       EOR   $0C
259D-   88          DEY
259E-   10 E7       BPL   $2587
25A0-   A8          TAY
25A1-   D0 0B       BNE   $25AE

; match address epilogue (one nibble)
25A3-   BD 8C C0    LDA   $C08C,X
25A6-   10 FB       BPL   $25A3
25A8-   C9 DA       CMP   #$DA    <-- !
25AA-   D0 02       BNE   $25AE
25AC-   18          CLC
25AD-   60          RTS
25AE-   38          SEC
25AF-   60          RTS

Now that the disk uses the standard
epilogue ($DE $AA $EB), we'll change
$DA to $DE.

This code is on sector $0E.

T00,S0E,$A9: DA -> DE

The data field parsing routine starts
immediately after, at $05B0 (in memory
now at $25B0):

*25B0L

25B0-   A6 30       LDX   $30
25B2-   A0 18       LDY   #$18
25B4-   88          DEY
25B5-   30 F7       BMI   $25AE

; match $D5 $AA $AD as normal
25B7-   BD 8C C0    LDA   $C08C,X
25BA-   10 FB       BPL   $25B7
25BC-   C9 D5       CMP   #$D5
25BE-   D0 F4       BNE   $25B4
25C0-   EA          NOP
25C1-   BD 8C C0    LDA   $C08C,X
25C4-   10 FB       BPL   $25C1
25C6-   C9 AA       CMP   #$AA
25C8-   D0 F2       BNE   $25BC
25CA-   A0 56       LDY   #$56
25CC-   BD 8C C0    LDA   $C08C,X
25CF-   10 FB       BPL   $25CC
25D1-   49 AD       EOR   #$AD
25D3-   D0 E7       BNE   $25BC

; ???
25D5-   08          PHP
25D6-   20 AC 05    JSR   $05AC
25D9-   28          PLP

*25ACL

25AC-   18          CLC
25AD-   60          RTS

Nothing. We're calling a subroutine to
do absolutely nothing except clear the
carry. And since we saved and restored
the status flags, we're not even doing
that.

Onward.

25DA-   88          DEY
25DB-   84 0B       STY   $0B
25DD-   BC 8C C0    LDY   $C08C,X
25E0-   10 FB       BPL   $25DD
25E2-   59 D6 02    EOR   $02D6,Y
25E5-   A4 0B       LDY   $0B
25E7-   99 00 03    STA   $0300,Y
25EA-   D0 EE       BNE   $25DA
25EC-   84 0B       STY   $0B
25EE-   BC 8C C0    LDY   $C08C,X
25F1-   10 FB       BPL   $25EE
25F3-   59 D6 02    EOR   $02D6,Y
25F6-   A4 0B       LDY   $0B
25F8-   99 00 02    STA   $0200,Y
25FB-   C8          INY
25FC-   D0 EE       BNE   $25EC
25FE-   BC 8C C0    LDY   $C08C,X
2601-   10 FB       BPL   $25FE
2603-   59 D6 02    EOR   $02D6,Y
2606-   D0 A6       BNE   $25AE

; also extremely weird
2608-   A1 00       LDA   ($00,X)

; match all three epilogue nibbles
260A-   BD 8C C0    LDA   $C08C,X
260D-   10 FB       BPL   $260A
260F-   C9 DA       CMP   #$DA    <-- !
2611-   D0 9B       BNE   $25AE
2613-   EA          NOP
2614-   BD 8C C0    LDA   $C08C,X
2617-   10 FB       BPL   $2614
2619-   C9 AA       CMP   #$AA
261B-   D0 91       BNE   $25AE
261D-   A4 2F       LDY   $2F
261F-   BD 8C C0    LDA   $C08C,X
2622-   10 FB       BPL   $261F
2624-   C9 EB       CMP   #$EB
2626-   D0 86       BNE   $25AE

; finish decoding disk nibbles into
; bytes in memory
2628-   A2 56       LDX   #$56
262A-   CA          DEX
262B-   30 FB       BMI   $2628
262D-   B9 00 02    LDA   $0200,Y
2630-   5E 00 03    LSR   $0300,X
2633-   2A          ROL
2634-   5E 00 03    LSR   $0300,X
2637-   2A          ROL
2638-   91 04       STA   ($04),Y
263A-   C8          INY
263B-   D0 ED       BNE   $262A
263D-   18          CLC
263E-   60          RTS

OK, the most obvious patch is the data
epilogue, which is now $DE $AA $EB.

T00,S0D,$10: DA -> DE

]PR#6
...reboots endlessly...

Hmm. Maybe because it's enforcing the
third data epilogue nibble?

; change the branch to the next line so
; it effectively ignores the third
; data epilogue nibble
T00,S0D,$27: 86 -> 00

Nope, still reboots endlessly. (I'll
keep the patch anyway.)

Hmm.

It's not the address prologue matching;
the LSR/EOR code to match $D4 or $D5
is a common trick. It still matches $D5
on any track, so it shouldn't require
any change.

After staring at this code for longer
than I would like to admit, it finally
dawned on me what this weird code
immediately after the data prologue is
for:

; cycles counts in margin, because that
; is the most important part
25CC-   BD 8C C0    LDA   $C08C,X   ; 4
25CF-   10 FB       BPL   $25CC     ; 2
25D1-   49 AD       EOR   #$AD      ; 2
25D3-   D0 E7       BNE   $25BC     ; 2

25D5-   08          PHP             ; 3
25D6-   20 AC 05    JSR   $05AC     ; 6

25AC-   18          CLC             ; 2
25AD-   60          RTS             ; 6

25D9-   28          PLP             ; 4

25DA-   88          DEY             ; 2
25DB-   84 0B       STY   $0B       ; 3
25DD-   BC 8C C0    LDY   $C08C,X   ;*3
25E0-   10 FB       BPL   $25DD

4+2+2+2+3+6+2+6+4+2+3+3 = 39, which is
too long. Each bit on disk takes 4 CPU
cycles to come around as the disk is
spinning.  That means we need to read
an 8-bit nibble every 32 cycles. The
data latch will hold the last value for
4 more cycles -- to compensate for the
fact that the read is actually done on
the third cycle of the 4-cycle LDA/LDY
instruction that hits the data latch
soft switch -- so we can spend an
absolute maximum of 36 cycles fetching
any one nibble. We've intentionally
wasted enough time that we'll miss the
first bit of the first nibble of the
data field.

Unless...

If the $AD, the third data prologue
nibble, has a timing bit after it, then
the data latch would hold that value
for 4 more cycles, and we would just
barely have enough time to catch the
first bit of the next nibble.

Turning back to the Copy II Plus nibble
editor, I can see it on the original
disk (originally shown in inverse,
which I converted to "+"):

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK: 01  START: 36E5  LENGTH: 188C

36C0: CD 9A D3 96 96 D3 AC E6   VIEW
36C8: 97 96 9D B5 F3 96 DA AA
36D0: EB 99 BF+D9+A7+F2 FF+FF+
36D8: B9 FF+FE+DC+FF+FF+FF+FC+
36E0: BE+FE+FF+FF+FF+D4 AA 96  <-36E5
                     ^^^^^^^^
                 address prologue

36E8: AA AA AA AB AA AB AA AA
      ^^^^^ ^^^^^ ^^^^^ ^^^^^
      V=000 T=$01 S=$01 chksm

36F0: DA AA EB 99 B5 FC D9 B7
      ^^^^^^^^
  address epilogue

36F8: F9 FE+FF+FF+D5 AA AD+FE
                  ^^^^^^^^^
                data prologue
               with timing bit
               after the third
                    nibble

                 --^--

At $05D5, I can put a "BEQ" to branch
over the JSR, which will save enough
time to make this code work on my
normalized disk with no extra timing
bits.

T00,S0E,$D5: 0820 -> F003

]PR#6
...still reboots endlessly...

I. Am. Still. Missing. Something.

                   ~

               Chapter 6
    Secret Agent Double-O... Zero?


After staring at this bootloader for an
embarrassing amount of time, looking at
the original disk in both a nibble and
a sector editor, I found another
difference: the disk volume number.

The original disk has a disk volume
number of 0 -- which, by the way, is
impossible to create with standard
tools. But as we've already seen, this
disk is anything but standard.

(My copy has a disk volume 254, the
default.)

But who cares? On an unprotected disk,
or even a protected disk with a
modified copy of DOS 3.3, the RWTS can
check and intentionally reject a disk
with the wrong disk volume number. This
bootloader does not contain that check.

Except...

At $061D, the custom RWTS is finishing
up matching the data field epilogue and
getting ready to finish decoding the
disk nibbles in the data field and
verifying the checksum before returning
a final boolean in the carry flag --
yes, this sector was read successfully,
or no, it wasn't.

``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``                                   .,
`` 261D-   A4 2F       LDY   $2F     .,
`` 261F-   BD 8C C0    LDA   $C08C,X .,
`` 2622-   10 FB       BPL   $261F   .,
`` 2624-   C9 EB       CMP   #$EB    .,
`` 2626-   D0 86       BNE   $25AE   .,
``                                   .,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,
``'-.,_,.-'``'-.,_,.='``'-.,_,.-'``'-.,

Look how we're initializing the Y
register: with zero page $2F. What's in
$2F? Why, we just set that, while we
were parsing the address field. It's
the disk volume number.

The disk volume number has nothing to
do with this part of the RWTS. The Y
register is used to look up into the
nibble translation table. In a standard
DOS 3.3 RWTS, that register is always
initialized to 0. So this is a very,
very, very sneaky way of ensuring that
the disk volume number is 0.

Which, of course, mine isn't.

The patch is to make the code do what
DOS 3.3 does: always initialize the Y
register to 0 going into the final
nibble decoding.

T00,S0D,$1D: A42F -> A000

]PR#6
...hangs with the drive motor off...

This is definitely progress. I mean, I
don't get to play the game or anything,
but it's no longer rebooting endlessly.
That means I'm now hitting an entirely
different way that this copy protection
is telling me to go f--- myself.

                   ~

               Chapter 7
           The 4shadow Knows


The RWTS now works. It is successfully
reading sectors and returning to the
caller. Where is the caller? Searching
for references to $0562, it appears the
caller is at $075F.

]PR#5
]BLOAD OBJ.0800-0BFF DECRYPTED,A$800
]CALL -151

*2500<900.BFFM

Now $0753 is in memory at $2753.

*275FL

; read and parse address field
275F-   20 62 05    JSR   $0562

; endlessly loop on error
2762-   B0 FB       BCS   $275F

; check if we're on the right track
; (I mean literally whether the track
; number is the expected value)
2764-   A5 2E       LDA   $2E
2766-   85 0D       STA   $0D
2768-   C5 0F       CMP   $0F

; if not, branch back and recalibrate
; (not shown)
276A-   D0 EE       BNE   $275A

; XOR with the first nibble of the
; address prologue ($D4 or $D5, set at
; $0569)
276C-   45 01       EOR   $01

; check the low bit
276E-   29 01       AND   #$01

; branch back if it doesn't match
2770-   F0 ED       BEQ   $275F

So even though the RWTS code itself
accepts a $D4 or $D5 for the first
address prologue nibble, the caller
then goes out of its way to check which
one it was. The even tracks need to be
$D5 $AA $96, and the odd tracks need
to be $D4 $AA $96.

Which, of course, is no longer true. On
my copy, every track uses the standard
$D5 $AA $96.

I can fool this check by changing the
"EOR" instruction to an "LDA", which
will ensure the "BEQ" never branches,
so it will never loop back to retry a
sector that already succeeded.

T00,S0C,$6C: 29 -> A9

Fun fact: the only time this bootloader
turns on the drive motor is in the
change track routine, which immediately
turn it off again. The rest of the RWTS
relies on the fact that the disk drive
won't actually turn off the motor when
it's told to. It lingers for about a
second -- long enough for the disk to
spin about 5 full revolutions. The code
assumes (correctly) that it can read
all the sectors on a track before the
drive motor actually turns off, at
which point it moves to the next track,
which turns the drive motor on again,
and the cycle repeats.

On my copy, the caller intentionally
rejects sectors based on the first
nibble of the address prologue, so it
keeps retrying the reads even though
they succeeded. Eventually the disk
drive motor really does turn off, at
which point nothing works, and we're
stuck in an endless loop of trying to
read a disk that is no longer spinning.

                   ~

               Chapter 8
   Chasing That Natural High (Score)


The disk still doesn't boot fully, but
we are absolutely, positively, most
definitely making progress. Now it
loads several tracks (probably the
entire game) before rebooting.

Because there is AN ENTIRELY OTHER RWTS
on the disk for the exclusive purpose
of reading and writing out high scores,
which are stored on T00,S09. This RWTS
is stored in reverse order on track
$04, but it looks like it gets loaded
into $B800 in memory. (I actually found
this RWTS by accident when I was
looking for the bootloader. Talk about
4shadowing!)

T04,S0F,$35: DA -> DE
T04,S0F,$91: DA -> DE
T04,S01,$9E: DA -> DE

]PR#6
...works, and it is glorious...

Quod erat liberandum.

                   ~

            Acknowledgments


Thanks to @DiskBlitz for the original
disk.

---------------------------------------
A 4am crack                    No. 1785
------------------EOF------------------
